这份文档怎么读

第一次接触 OPC UA?别慌,这份文档从零开始,全程用生活化的比喻讲解。

新手路线
先读第 1~5 节"协议入门",建立直觉;再看第 6 节起的"驱动架构"了解实现。
比喻速查
地址空间=图书馆、NodeId=索书号、订阅=公众号、安全通道=过安检。
避坑提醒
看到橙色/红色提示框要特别注意——那是踩过坑总结的经验。
只做 Client
本驱动是 OPC UA Client,只连接 Server 取数据/发指令,不提供 Server 能力。
0

一张图看懂 OPC UA 驱动

全局视角:从设备到平台的完整链路

先看这张全景图建立整体认知,再看后续细节章节就不会"只见树木不见森林"。核心就一句话:驱动作为 Client 连到 Server,把数据采回来推给平台。

flowchart TB PLC["PLC / SCADA / 网关
运行 OPC UA Server"] CONNECT["连接管理
Discovery → 安全通道 → Session"] COLLECT["数据采集
订阅 / 轮询 / 混合"] WRITE["指令下发
Write + READ_BACK 确认"] PC["PlatformClient
唯一通道"] QUEUE["点位队列"] BUSINESS["实时缓存 · 规则 · 告警
WebSocket · 历史库"] PLC -.->|"opc.tcp 连接"| CONNECT CONNECT --> COLLECT CONNECT --> WRITE COLLECT -->|"pushPointValue"| PC PC --> QUEUE --> BUSINESS BUSINESS -.->|"issuePointValue"| WRITE WRITE -.->|"Write 指令"| PLC style PLC fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style CONNECT fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style COLLECT fill:#f0e8fe,stroke:#5856d6,color:#1c1c1e style WRITE fill:#fff3e0,stroke:#ff9500,color:#1c1c1e style PC fill:#e0f7fa,stroke:#5ac8fa,color:#1c1c1e style QUEUE fill:#e0f7fa,stroke:#5ac8fa,color:#1c1c1e style BUSINESS fill:#fce8ec,stroke:#ff2d55,color:#1c1c1e
设备层
PLC 等运行 OPC UA Server,提供数据源
驱动层
连设备采数据、转发平台指令
平台层
缓存/规则/告警/历史等业务
边界
驱动只通过 PlatformClient 读写平台
新员工的三条阅读路线
我是纯新手
① 这张图看懂全局 →
② 第 1-5 节协议入门建立直觉 →
③ 第 6-7 节看实例/点位属性 →
④ 跳到「5 分钟极简教程」动手
我要接入设备
① 看协议选型对比确认 OPC UA →
② 厂商适配指引查注意事项 →
③ 照「从零接入一台 Server」17 步操作 →
④ 出问题查「常见问题速查」
我要读源码
① 第 6 节驱动架构看分层 →
② 核心类导航找入口类 →
③ 结合数据采集链路理解流转 →
④ 安全档案章节看凭据实现

OPC UA 协议入门指南

从零开始理解工业自动化的「通用语言」 · IEC 62541

1

什么是 OPC UA

从一个例子说起

想象一个现代化工厂的车间,里面有各种设备:西门子 PLC、贝加莱控制器、各种品牌的传感器和仪表。每台设备都在产生数据——温度、压力、产量、能耗……但它们由不同厂家生产,各自"说着不同的方言",数据格式五花八门。

→ OPC UA 就是让所有工业设备"说同一种语言"的翻译官——工业 4.0 时代的"普通话"。

一句话记住

没有 OPC UA 之前,对接西门子 PLC 得用它的私有协议,换一台倍福的 PLC 又得换一套协议——集成商疲于奔命。OPC UA 出现后,大家"都说普通话",一套接口通吃主流工业设备。

OPC UA 是什么

OPC UA(OPC Unified Architecture,OPC 统一架构)是专为工业自动化设计的跨平台开放标准通信协议,由 OPC 基金会制定,国际标准编号 IEC 62541。它是经典 OPC(基于 Windows COM/DCOM)的全面升级,"UA" 就是"统一架构"的意思。

名字小故事:最初的 "OPC" 是 "OLE for Process Control"(基于微软 OLE 的过程控制),但 UA 版本已经完全脱离 Windows,跨平台、跨语言,名字只是个历史符号了。

Client 与 Server 的分工
角色通俗理解本驱动是谁
OPC UA Server提供数据的"服务窗口"——PLC、SCADA、网关、第三方软件—
OPC UA Client主动去"取数据/发指令"的客户端本驱动
驱动实例页面里配置的一条 Server 连接1 实例 = 1 Server 连接
VariableServer 里能采集/下发的数据点导入后变成平台点位

重要:本驱动是 OPC UA Client,不提供 Server 能力。

应用领域
PLC 通信
西门子 · 贝加莱 · 倍福 · 施耐德
SCADA 系统
WinCC · Ignition · KEPServerEX
工业网关
汇聚底层协议 · 转换上报
能源管理
电表 · 水表 · 汽表数据
MES / ERP 对接
生产执行系统数据采集
物联网网关
CODESYS · 边缘计算设备
2

地址空间——Server 的「图书馆」

地址空间像一座图书馆

想象 Server 内部有一座巨大的图书馆,里面所有的数据点都是"书"。这些书按树状结构分类摆放——有"楼层"(Objects 文件夹)、有"书架"(子文件夹)、有"书"(Variable 数据点)。

要找一本书,你需要它的索书号。在 OPC UA 里,这个"索书号"叫 NodeId。

Node 与 NodeId——每本书的索书号

Server 里所有东西都是 Node(节点)——数据点、文件夹、类型、视图……每个 Node 都有一个 NodeId 来定位自己。NodeId 由两部分组成:

NamespaceIndex
命名空间下标
一个数字,类似"书在第几阅览室"
例:ns=2
Identifier
标识内容
数字或字符串,类似"书在该阅览室的编号"
例:s=Machine/Temperature

完整写法:ns=2;s=Machine/Temperature

为什么不能只存 ns=2

关键陷阱:NamespaceIndex 不稳定!
Server 重启、升级、加载新信息模型后,原来的 ns=2 可能变成 ns=3。如果你把 ns=2;s=Temperature 死死存进数据库,下次连接可能就找错位置,甚至读到完全不相干的数据。

本驱动的解决:保存稳定的 NamespaceUri(如 urn:factory:line1),每次连接后查表反查当前编号,编号怎么变都不影响点位。

这是最容易踩的坑

很多老系统直接存 ns=2;s=Temperature,Server 一重启编号变了,点位全失效甚至读到错误数据。本驱动从设计上规避了这个问题——你配置点位时填的是稳定的 NamespaceUri,不用关心数字编号。

Variable——真正能采集的数据点

地址空间里有多种 Node,但本驱动只关心 Variable——只有 Variable 才有"当前值"可供采集和下发。文件夹(Object)、类型(Type)、方法(Method)等都不能直接采值。

graph TB ROOT["Root 根目录"] --> OBJ["Objects 楼层"] OBJ --> MACH["Machine 机床文件夹
Object(不可采值)"] MACH --> TEMP["Temperature 温度
Variable ✓ 可采值"] MACH --> HUMI["Humidity 湿度
Variable ✓ 可采值"] MACH --> SETP["Setpoint 设定值
Variable ✓ 可采值"] style ROOT fill:#f2f2f7,stroke:#8e8e93,color:#1c1c1e style OBJ fill:#f0e8fe,stroke:#5856d6,color:#1c1c1e style MACH fill:#fff3e0,stroke:#ff9500,color:#1c1c1e style TEMP fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style HUMI fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style SETP fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
浏览(Browse)——在书架间翻找

你不知道 Server 里有什么数据点时,就需要 Browse——从根目录开始,一层层"翻文件夹",直到找到需要的 Variable。就像在图书馆查目录卡,定位到具体的书,再把它"借出来"(import)变成平台点位。

导入点位的标准姿势

本驱动不会自动扫描整台 Server。正确流程是:Browse 逐层展开 → 人工选中需要的 Variable → 调 importNodes 导入。这保证你只采集关心的点,不会把 Server 里成千上万个无关节点全拖进来。

3

连接三步曲——从敲门到坐下

Discovery——先看门口的告示牌

连 Server 前,先要知道它开放了哪些入口、要求什么安全策略、接受什么身份。这就是 Discovery——相当于看 Server 门口的"营业告示牌"。返回的每一条信息叫一个 Endpoint,告诉你:

Endpoint 包含的信息通俗理解
Endpoint URL"本入口的地址",如 opc.tcp://10.0.0.8:4840
SecurityPolicy"用什么加密算法",如 NONE / Basic256Sha256
MessageSecurityMode"要不要签名/加密",如 NONE / SIGN / SIGN_AND_ENCRYPT
支持的 IdentityType"允许谁进",如 ANONYMOUS / USERNAME / X509
Server 证书指纹"Server 的身份证号"(SHA-256)
SecureChannel——过安检

选好 Endpoint 后,第二步建立 SecureChannel(安全通道)。这一步会:
· 校验 Server 证书是否可信
· 协商加密算法
· 如果用证书,Client 也要出示自己的证书

相当于"过安检"——确认双方身份、约定好怎么加密通信。

Session——登记入座

安检过后,第三步建立 Session(会话)。Session 是 Client 登录后获得的"长期通行证",期间的所有读写订阅都在这个会话上进行。

相当于"登记入座"——之后所有的服务请求都凭这张通行证办理。Session 有超时时间,需要定期"续证"(KeepAlive)。

三种身份(Identity)
身份类型通俗理解需要提供能否用无安全 Endpoint
ANONYMOUS 匿名"访客"——谁都能进无需任何凭据可以
USERNAME 用户名密码"持票观众"——出示门票username + 密码引用禁止
X509 证书"VIP 持卡人"——刷卡进入客户端证书 + 安全档案禁止

安全规则:USERNAME 不允许通过无加密的 Endpoint 发送(密码会明文泄露)。

三步连接小结
打个比方:去政府办事

Discovery = 看大厅公告牌(哪些窗口开放、需要什么材料)→ SecureChannel = 过安检门(出示身份证、包过扫描仪)→ Session = 到窗口取号排队办事(之后所有业务都在这个号下办理)。三步缺一不可,顺序也不能乱。

安全等级组合

SecurityPolicy(加密算法)和 MessageSecurityMode(消息模式)必须配对使用,组合规则严格:

graph LR NONE_P["SecurityPolicy = NONE"] --> NONE_M["Mode = NONE
不加密不签名"] SIGN_P["非 NONE 安全策略"] --> SIGN_M["Mode = SIGN
仅签名"] SIGN_P --> SE_M["Mode = SIGN_AND_ENCRYPT
签名+加密
推荐"] style NONE_P fill:#f2f2f7,stroke:#8e8e93,color:#1c1c1e style NONE_M fill:#f2f2f7,color:#1c1c1e style SIGN_P fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style SIGN_M fill:#e0f7fa,stroke:#5ac8fa,color:#1c1c1e style SE_M fill:#e8f0fe,stroke:#007aff,color:#1c1c1e

支持的策略:NONE、Basic128Rsa15、Basic256、Basic256Sha256、Aes128_Sha256_RsaOaep、Aes256_Sha256_RsaPss。实际可用以 getEndpoints 返回为准。

4

订阅——Server 的「公众号推送」

方式一:轮询(POLLING)

Client 每隔 N 秒主动问 Server:"温度是多少?"
Server 答:"25.6"。
N 秒后再问:"温度是多少?"
Server 答:"25.6"。
问题:大部分时候没变化,白白浪费带宽;且最多有 N 秒延迟。

方式二:订阅(SUBSCRIPTION)

Client 跟 Server 说:"帮我盯着温度,变化超过 0.5 就告诉我。"
Server 答:"好的。"
温度从 25.6 变为 26.2(变化 0.6 > 0.5):
Server 主动推送:"温度变了,现在 26.2!"
优势:不浪费带宽,几乎零延迟——就像关注了"公众号"自动收推送。

订阅的工作原理

订阅机制分两层:Subscription(订阅)是"推送频道",MonitoredItem(监控项)是频道里"具体被盯着的点"。一个 Subscription 可以挂多个 MonitoredItem。

sequenceDiagram participant C as Client(本驱动) participant S as OPC UA Server C->>S: 1 创建 Subscription(发布周期 1s) S-->>C: 确认 订阅成功 C->>S: 2 添加 MonitoredItem(盯温度,变化阈值 0.5) S-->>C: 确认 监控成功 Note over S: 温度持续被采样 S->>C: 3 值变化超阈值时推送 DataChange Note over C: 收到后转换并推平台

两个关键参数:发布周期(Server 多久推一次通知)、采样周期(Server 多久测一次值)。用户配的是 requested 值,Server 可能修订为它能支持的实际值。

三种采集模式对比
对比维度SUBSCRIPTION 订阅POLLING 轮询HYBRID 混合
原理Server 主动推变化Client 定时全量读订阅 + 定时核对
实时性高——变化即推取决于轮询周期高
网络开销低——仅推变化高——每次全读较高
可靠性依赖 Server 订阅实现简单可靠最强(订阅漏报有核对兜底)
适用场景大多数正式 Server订阅不稳的 Server强一致性要求场景
默认本驱动默认

策略:SUBSCRIPTION 模式下,单个 MonitoredItem 创建失败的点会自动降级为轮询,不影响其他点。

怎么选?

大多数情况用默认的 SUBSCRIPTION。如果发现订阅不更新(某些 Server 实现不完整),切 POLLING 验证是不是 Server 的问题;如果对数据一致性要求极高(不能漏报),用 HYBRID 双保险。

数据质量(StatusCode)

每个采样值都附带一个 StatusCode,表示这个数据"靠不靠谱"。OPC UA 分三档,本驱动映射到平台的 statusFlag:

OPC UA 质量含义平台 statusFlag
Good数据可靠,可直接使用1
Uncertain数据存疑(如传感器超出量程)7
Bad数据不可用(如通信失败)2

Bad 且没有可用值的样本,本驱动不会向平台推 null,只保留原始诊断信息。

5

OPC UA 协议特点总结

统一信息模型
地址空间 + Node + NodeId,结构标准化,跨厂商通用
Discovery 自发现
先查 Endpoint 再连接,无需猜测安全配置
订阅推送
值变化主动推,实时高、开销低,类似公众号
内置安全
证书 + 签名 + 加密,工业级安全通信
跨平台开放
脱离 Windows,纯 TCP 实现,Linux/嵌入式通用
数据质量分级
Good / Uncertain / Bad 三档,可信度透明
1

驱动整体架构

平台加载过程

OptiCloud 启动时,通过 Java SPI 机制自动发现并加载驱动。OPC UA 驱动的注册入口在 SPI 文件中固定一行:

flowchart LR A["应用启动"] --> B["DriverAccessLoader"] B --> C["ServiceLoader 读取 SPI"] C --> D["OpcUaAccess"] D --> E["注入 PlatformClient"] D --> F["注册 4 个驱动 API"] D --> G["注册 4 秒 AliveTask"] D --> H["创建 OpcUaInstanceManager"] style D fill:#e8f0fe,stroke:#007aff,color:#1c1c1e
运行时分层

驱动采用清晰的分层设计,每层职责单一。每个实例独立连接、独立 Session、独立 Subscription,互不影响——即使 Endpoint 相同也不跨项目共享。

graph TB ACCESS["OpcUaAccess
SPI 入口,只做参数补全和委托"] MGR["OpcUaInstanceManager
管理全部实例、心跳对账、API 路由"] H1["Holder #101
单实例状态机"] H2["Holder #202
单实例状态机"] H3["Holder #303
单实例状态机"] ACCESS --> MGR MGR --> H1 & H2 & H3 H1 --> C1["Milo Client / Session / Subscription"] H2 --> C2["Milo Client / Session / Subscription"] H3 --> C3["Milo Client / Session / Subscription"] style ACCESS fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style MGR fill:#f0e8fe,stroke:#5856d6,color:#1c1c1e style H1 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style H2 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style H3 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
层职责
Access平台 SPI 入口,只做参数补全和委托
Manager管理全部实例、实例级锁、退避、批量写入分组、API 路由
Holder一个实例的完整运行状态:连接、点位快照、订阅、轮询、推送、重连
Core协议操作和类型转换,不感知 Web Controller
PlatformClient驱动访问平台数据的唯一通道
数据采集链路
flowchart LR A["OPC UA Server"] --> B["Subscription 回调
或 Polling Read"] B --> C["OpcUaSampleConverter
类型/质量转换"] C --> D["LatestValueBuffer
按 pointId 合并最新"] D --> E["500ms 虚拟刷新线程"] E --> F["PlatformClient.pushPointValue"] F --> G["PointQueueManager"] G --> H["缓存/规则/告警/
WebSocket/历史链路"] style C fill:#e0f7fa,stroke:#5ac8fa,color:#1c1c1e style D fill:#fff3e0,stroke:#ff9500,color:#1c1c1e

关键设计:Milo 回调线程只做转换和缓冲,不直接调用平台,防止平台慢操作阻塞 OPC UA Publish 线程。

为什么中间要加个"缓冲"

想象快递员(Milo 回调)和分拣中心(平台)。如果快递员每收一个包裹就亲自跑去送客户(直接调用平台),路上耽误的时间会让后面的包裹堆积。加个"暂存架"(LatestValueBuffer),快递员放下就走,专门的配送员(500ms 刷新线程)批量送——效率高,谁也不堵谁。

核心类导航
路径 / 类作用
access/OpcUaAccessDriverAccess 实现和 SPI 入口
access/OpcUaInstanceManager多实例生命周期、心跳对账、API 和写入分发
access/OpcUaInstanceHolder单实例状态机、采集线程、重连和热更新
core/OpcUaConfigParser所有字符串属性的强类型解析和统一校验
core/OpcUaClientFactoryDiscovery、Endpoint 选择、安全身份、连接
core/OpcUaNodeResolverNamespaceUri 稳定地址与 NodeId 转换
core/OpcUaBrowser分页 Browse 和 ContinuationPoint 释放
core/OpcUaNodeImporterServer 元数据复核和平台散点创建
core/OpcUaPointReader批量 Read 和 TooManyOperations 自动折半
core/OpcUaSubscriptionControllerSubscription、MonitoredItem 和单点轮询降级
core/OpcUaPointWriter批量 Write、逐点 StatusCode 和 READ_BACK
storage/OpcUaLatestValueBuffer每点只保存最新待推送样本
task/OpcUaAliveTask每 4 秒实例/点位对账、心跳和重连
2

实例属性(22 项)

连接与安全属性(8 项)
属性 code默认值说明
endpointUrl必填实际 Endpoint URL,如 opc.tcp://10.0.0.8:4840
serverVendorAUTOAUTO / GENERIC / SIEMENS / BECKHOFF / KEPSERVEREX / CODESYS / IGNITION / SCHNEIDER / CUSTOM
securityPolicyNONEServer Endpoint 的安全策略
messageSecurityModeNONENONE / SIGN / SIGN_AND_ENCRYPT
identityTypeANONYMOUSANONYMOUS / USERNAME / X509
username空USERNAME 时必填
passwordSecretRef空正式 USERNAME 必填,只保存引用不存明文
securityProfileId空安全 Endpoint 或 X509 时必填

安全组合铁律:NONE 策略只能配 NONE 模式;非 NONE 必须启用 SIGN 或 SIGN_AND_ENCRYPT;USERNAME 禁止用无安全 Endpoint。

不要凭记忆填安全参数

安全组合的正确来源只有一个:先调 getEndpoints 看 Server 实际公告什么,再把选中的完整组合存进实例。千万不要手猜 SecurityPolicy URI——猜错了连接必然失败,而且报错信息不一定直观。

超时·采集·写入·重连(14 项)
属性 code默认值范围 / 说明
connectionTimeoutMs100001000 ~ 120000
requestTimeoutMs100001000 ~ 120000
sessionTimeoutMs6000010000 ~ 3600000
collectModeSUBSCRIPTIONSUBSCRIPTION / POLLING / HYBRID
publishingIntervalMs100050 ~ 60000,Server 可修订
defaultSamplingIntervalMs10000 ~ 60000,0 表示尽可能快
defaultQueueSize11 ~ 1000
discardOldesttrue队列满时是否丢弃最旧值
pollIntervalMs5000100 ~ 3600000
batchSize1001 ~ 300
initialReadEnabledtrue建立采集器前读取当前值
writeConfirmModeREAD_BACKNONE / READ_BACK
reconnectEnabledtrue是否允许自动重连
maxReconnectDelayMs600004000 ~ 300000
三种采集模式详解
SUBSCRIPTION(默认)
订阅成功的点由 Server 推送;单点失败自动轮询降级,不拖垮整组
POLLING
所有可读点定时批量 Read,不依赖 Server 订阅实现
HYBRID
订阅为主 + 定期全量核对,可靠性和诊断能力最强
选择建议
优先 SUBSCRIPTION;订阅不更新时换 POLLING 排查;强一致性需求用 HYBRID
3

点位属性(12 项)

点位属性定义
属性 code说明
namespaceUri稳定命名空间 URI,必填(替代易变的 ns 数字)
identifierTypeNUMERIC / STRING / GUID / OPAQUE
identifierNodeId 标识内容;OPAQUE 使用 Base64
attributeId固定 VALUE(当前只采值属性)
indexRange必须为空(不支持数组切片)
samplingIntervalMs可选,覆盖实例默认,0 ~ 60000
queueSize可选,覆盖实例默认,1 ~ 1000
discardOldest可选,覆盖实例默认
deadbandTypeNONE / ABSOLUTE / PERCENT
deadbandValueABSOLUTE 非负;PERCENT 为 0 ~ 100
serverDataTypeNodeId导入时记录的诊断信息,运行时以 Server 为准
valueRank固定 -1(表示标量,不支持数组)

PERCENT 死区依赖 Variable 的 EURange,缺 EURange 时该点会标记失败并转轮询。

稳定地址——为什么用 NamespaceUri

本驱动持久化点位时,不保存易变的 ns=2,而是保存四元组:

namespaceUri + identifierType + identifier + attributeId

每次连接成功后读取 NamespaceArray,把 NamespaceUri 解析回当前的 NodeId。这样即使 Server 重启导致编号从 ns=2 变成 ns=3,点位也无需修改,自动重新定位。

数据类型映射
OPC UA 类型平台类型
Booleanbool
SByte / Byte / Int16 / UInt16 / Int32int
Float / Doubledouble
Enumerationenum
UInt32 / Int64 / UInt64text(避免精度损失)
String / XmlElement / LocalizedText / Guid / DateTimetext

DateTime 转为 UTC ISO-8601;数组、ByteString、Structure 不做隐式 toString。

点位唯一码

点位唯一码格式(名称不参与判断,改名不重建):

opcua:{instanceId}:{SHA-256(namespaceUri|identifierType|identifier|VALUE)}

幂等保证:同一节点重复导入返回 SKIPPED,不会创建第二个点位。

4

四大驱动 API

API 总览

统一路由前缀 POST /driver/api/opcua/{api},请求使用表单参数(nodes 是 JSON 数组字符串)。Controller 自动从项目上下文写入 projectId。

API作用前置条件
getEndpointsDiscovery,列出 Endpoint 组合不需要实例
testConnection临时试连,结束立即关闭不需要正式实例
browseNodes用正式 Session 分页浏览节点树实例 RUNNING
importNodes复核元数据并保存为散点实例 RUNNING
getEndpoints——发现入口

输入 Discovery URL,返回 Server 公告的所有 Endpoint(URL、安全策略、消息模式、身份类型、证书指纹)。

正确顺序:先 getEndpoints → 用户选择完整组合 → 保存到实例。
不能默认用返回列表第一项。

testConnection——临时试连

用与正式连接相同的配置建立一次临时连接,读取 NamespaceArray 和 ServerInfo。

关键:返回前一定在 finally 中关闭临时 Client。成功只证明参数能建立连接,不能用于后续浏览/采集/下发。

browseNodes——浏览节点树
参数默认值说明
instanceId必填必须属于当前项目且 RUNNING
expandedNodeIdRootFolder要展开的节点(稳定 ExpandedNodeId)
directionFORWARDFORWARD / INVERSE / BOTH
nodeClasses全部逗号分隔,如 OBJECT,VARIABLE
pageSize1001 ~ 200
continuationToken空下一页随机 token,只能消费一次
releaseContinuationPointfalsetrue 表示取消分页并释放 Server 游标

返回节点的 selectable=true 表示当前用户可读/可写的标量 Variable——只有这种才能导入。

importNodes——导入点位

提交 Browse 选中的 ExpandedNodeId,驱动重新从 Server 读取真实元数据(NodeClass、DataType、权限、单位、量程),只导入可读/可写的标量 Variable。

CREATED
已创建平台散点
SKIPPED
已有点位或批内重复(幂等)
FAILED
类型/权限/死区/容量等不符合
部分成功
success=true,失败明细保留在 items

导入只创建散点,不创建设备类型/设备/自动绑定,不修改数据库表结构。

三种连接的本质区别(重要)
操作是否建 SessionSession 是否保留能否证明正式实例已连接
getEndpoints否否否
testConnection是否(finally 关闭)否(只证明参数可用)
openDriverInstance是是(长期持有)需配合状态+Browse 验证

正式连接成功的三项铁证:① 日志"实例已连接" ② 一个心跳后 instanceStatus=1 ③ browseNodes 能浏览到节点。

5

实例生命周期与状态机

实例状态机
stateDiagram-v2 [*] --> NEW NEW --> CONNECTING: open CONNECTING --> RUNNING: Session/点位/采集器成功 CONNECTING --> OFFLINE: 连接失败 RUNNING --> OFFLINE: Session 或 Publish 失效 OFFLINE --> RECONNECTING: 退避到期 RECONNECTING --> RUNNING: 重连成功 RECONNECTING --> OFFLINE: 重连失败 RUNNING --> CLOSING: close/update OFFLINE --> CLOSING: close CLOSING --> CLOSED
状态值状态含义
0CLOSED实例开关关闭,没有正式 Session
1RUNNINGHolder、Client 和正式 Session 正常
3OFFLINE已启用但连接不可用,或仍在退避重试
关闭顺序(7 步)
flowchart TD S1["1 状态切换为 CLOSING
增加运行代次"] --> S2["2 停止轮询和刷新虚拟线程
并等待退出"] S2 --> S3["3 删除 Subscription"] S3 --> S4["4 清理最新值缓冲和点位快照"] S4 --> S5["5 释放全部 Browse ContinuationPoint"] S5 --> S6["6 关闭 Session 和 Milo Client"] S6 --> S7["7 状态切换为 CLOSED"] style S1 fill:#fff3e0,stroke:#ff9500,color:#1c1c1e style S7 fill:#f2f2f7,stroke:#8e8e93,color:#1c1c1e

运行代次(generation)机制:阻止旧回调在新连接建立后继续推值,避免"幽灵数据"。

心跳与退避

AliveTask 每 4 秒检查实例、Session、Subscription Publish、点位指纹和凭据 revision。失败按 4/8/16/32/60 秒退避,受 maxReconnectDelayMs 限制,避免高频冲击 Server。

flowchart LR F1["失败1次
等待4秒"] --> F2["失败2次
等待8秒"] F2 --> F3["失败3次
等待16秒"] F3 --> F4["失败4次
等待32秒"] F4 --> F5["失败5+次
等待60秒封顶"] F5 --> RCV{"重连成功?"} RCV -->|"Yes"| RST["重置退避
恢复采集"] RCV -->|"No"| F5 style F1 fill:#fff3e0,stroke:#ff9500,color:#1c1c1e style F5 fill:#ffe5e5,stroke:#ff3b30,color:#1c1c1e style RST fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
为什么越失败等越久

如果 Server 挂了,每 4 秒重连一次等于持续"砸门"——既浪费资源,也可能拖垮正在恢复的 Server。退避策略像"敲门没人应,下次多等一会儿再敲":第一次等 4 秒,再失败等 8 秒……最长 60 秒一次。Server 恢复后立即重置,马上恢复采集。

6

点位下发流程

写入流程(13 步)
flowchart TD P1["1 平台 issuePointValueBatch"] --> P2["2 Manager 按 driverInstanceId 分组"] P2 --> P3["3 Holder 必须为 RUNNING"] P3 --> P4["4 校验 projectId/instanceId/pointId"] P4 --> P5["5 校验平台 accessMode 含 w
+ Server UserAccessLevel"] P5 --> P6["6 按实时 Server DataType 严格转换"] P6 --> P7["7 批量 Write 并逐项检查 StatusCode"] P7 --> P8["8 READ_BACK 模式只读 Write 成功项"] P8 --> P9["9 规范化目标值 = 读回值 才算成功"] style P1 fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style P9 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e

只读、错误类型、越界或 Bad StatusCode 都不会被记为成功。

为什么要"回读"才算成功

有些设备收到 Write 指令返回"成功",但内部业务逻辑会改写或钳位你的值(比如设 100 但设备上限是 80)。如果只看 Write 结果,平台会显示一个从未真实生效的值。READ_BACK 模式重新读一次真实值,平台显示的永远是设备里真实的数——宁可显示失败,不报虚假的成功。

READ_BACK(默认·推荐)

Write 成功后重新读 Server 实际值,规范化比较目标值与读回值,相等才推送确认值到平台。

优势:平台显示的值是 Server 真实生效的值,不是乐观假设。

NONE

只检查 Write 的 StatusCode,不回读。

注意:不会把目标值乐观推入平台,需等待后续订阅或轮询更新。适合对实时确认不敏感的场景。

7

安全档案与凭据管理

密码引用(Secret)——只存"钥匙牌号"

USERNAME 连接需要密码,但本驱动绝不存明文密码。密码加密保存在密钥环里,实例属性只保存一个 secretRef 引用——相当于只记下"钥匙牌的编号",不存钥匙本身。

graph LR A["真实密码"] -->|"加密保存"| B["密钥环
opti.opcua.security"] B -->|"生成引用"| C["secretRef
如 sec_xxx"] C -->|"实例属性只保存这个"| D["passwordSecretRef"] style A fill:#ffe5e5,stroke:#ff3b30,color:#1c1c1e style B fill:#f0e8fe,stroke:#5856d6,color:#1c1c1e style C fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style D fill:#e8f0fe,stroke:#007aff,color:#1c1c1e

接口:POST /iot/opcua/security/secrets(创建)· PUT .../secrets/{secretRef}(轮换)· DELETE(删除,被引用时拒绝)。

安全档案(Profile)——证书的保险柜

安全 Endpoint 需要客户端证书建立 SecureChannel。安全档案是集中保管证书的容器,使用 multipart/form-data 上传:

字段必填含义
profileName必填档案显示名
applicationUri可选应与客户端证书 SAN URI 一致
pkcs12File必填含客户端私钥+证书链的 PKCS12/PFX
pkcs12Password必填PKCS12 文件密码
trustedCertificates可选目标 Server 证书或签发 CA(可多文件)
crlFiles可选证书吊销列表

接口会校验 PKCS12 可读性、私钥匹配、证书有效期、Application URI、信任证书等。返回不含私钥/PKCS12/密码正文。

安全设计:只存"牌号"不存"钥匙"

实例属性里保存的 passwordSecretRef 和 securityProfileId 只是"引用编号",真实密码和私钥加密存在密钥环里。这样即使数据库泄露,攻击者拿到的也只是一堆无意义的编号,无法还原出原始凭据。

凭据轮换——不停机更新

Secret 或 Profile 支持在实例运行时直接更新,不需要手动停实例。但更新后驱动会对引用该材料的实例做一次冷重连——期间可能短暂断采:

flowchart LR A["更新 Secret/Profile"] --> B["事务提交"] B --> C["关闭旧 Subscription/Session"] C --> D["临时 OFFLINE/RECONNECTING"] D --> E["重新解析新材料"] E --> F["重新 Discovery + Endpoint 选择"] F --> G["建新 Session + 重读元数据"] G --> H["恢复 RUNNING"] style A fill:#fff3e0,stroke:#ff9500,color:#1c1c1e style H fill:#e8f8e8,stroke:#34c759,color:#1c1c1e

建议先 POST .../profiles/{id}/validate 校验材料再轮换;轮换失败实例保持 OFFLINE,需修正材料后重试。

证书从哪来——三类文件职责

生产环境安全连接涉及三类文件,不是同一个东西,也不能相互推导:

文件内容用途
server.derServer 公钥证书上传到 trustedCertificates,校验 Server 身份
client.p12Client 私钥+证书链上传到 pkcs12File,建立 SecureChannel
client.derClient 公钥证书导入 Server 信任列表(由 Server 管理员操作)

必须建立双向信任:Client 信任 server.der;Server 信任 client.der(或其签发 CA)。

8

部署与测试

环境要求
JDK21+
协议库Eclipse Milo Client 0.6.16
驱动版本1.0.0
驱动名固定 opcua
运行方式普通 JAR + target/lib
接入模式EMBEDDED(内嵌,共享 JVM)
内置 Milo 测试 Server

应用启动时自动起一个本机 Server 用于联调:

Endpoint: opc.tcp://localhost:4840/opti-test
NamespaceUri: urn:sctech:opti:opcua:long-running-test
账号 / 密码: 见内部交付物(测试凭据不写入公开文档)

只绑定 127.0.0.1,仅用于本机联调;不代表真实厂商支持 Basic256Sha256。

测试节点说明
节点类型accessModeServer 行为
Temperaturedouble只读20.0~29.9 循环变化,验证订阅
Humiditydouble只读固定值
Setpointdouble读写默认值稳定,适合下发测试
Runningbool读写开关状态
Counterint读写每秒递增
Messagetext读写字符串
9

常见问题速查

连接类问题
现象检查方向
getEndpoints 失败URL 是否绝对 URI;端口/防火墙/DNS;Server 是否启动
"未找到完全匹配的 Endpoint"保存的 URL/策略/模式/身份与 Discovery 返回不一致;不要手猜 SecurityPolicy
USERNAME/X509 被拒绝CredentialProvider 是否安装;secretRef/profileId 是否属当前项目;证书有效期/信任链
实例 RUNNING 但没数据initialRead 是否返回值;订阅点是否降级轮询;临时切 HYBRID 排查 Publish 兼容性
导入类问题
现象检查方向
只支持 VariableObject/Method/Type 不能导入
valueRank 必须 -1数组/矩阵不支持
权限不足当前用户需 CurrentRead 或 CurrentWrite
PERCENT 死区失败要求 Server 提供 EURange
容量不足有效节点整批不保存
采集类问题
现象检查方向
浏览 token 无效token 只能用一次;120 秒过期;实例重连后旧 token 失效
订阅不更新测试 Server 的 SubscriptionModel 未委托;切 POLLING 验证 Server 节点正常
实时值有但无历史驱动不写 InfluxDB;检查平台历史开关/PointQueueManager/InfluxDB 配置
下发类问题
现象检查方向
"点位的驱动实例未运行"查 paginatePoint 记录 driverInstanceId;查 getDriverWithInstanceList 确认该实例 instanceStatus=1(不是同名/同项目其他实例)
写入失败accessMode 是否含 w;UserAccessLevel 是否含 CurrentWrite;类型能否严格转换;READ_BACK 是否读到不同值
HTTP success=true 但值没变接口 success 只表示"已调用驱动",必须看 curValue/curValueTime/日志

排查"实际运行但提示未运行"的关键:以 paginatePoint 的 driverInstanceId + getDriverWithInstanceList 的 instanceStatus=1 为准,不要凭页面图标或同名实例判断。

10

从零接入一台 Server

为什么要看这一节

前面的章节讲了 OPC UA 的概念和单个 API,但新员工最常问的是:"我到底该按什么顺序操作?" 这一节把从零到采到数据的完整流程串成一条线,照着做就能跑通。

12 步走完全程

下面以平台内置的 Milo 测试 Server 为例(opc.tcp://localhost:4840/opti-test),完整演示从查驱动 ID 到验证采集的每一步。所有 HTTP 请求都要带登录认证 + Project-Id 请求头。

完整流程总览
flowchart TD S1["① 确认 Server 已启动"] --> S2["② 查询 driverId"] S2 --> S3["③ getEndpoints 发现 Endpoint"] S3 --> S4["④ testConnection 临时试连"] S4 --> S5["⑤ saveInstance 保存关闭实例"] S5 --> S6["⑥ 查询 instanceId"] S6 --> S7["⑦ openDriverInstance 启动"] S7 --> S8["⑧ 确认 instanceStatus=1"] S8 --> S9["⑨ browseNodes 浏览节点"] S9 --> S10["⑩ 收集 ExpandedNodeId"] S10 --> S11["⑪ importNodes 导入点位"] S11 --> S12["⑫ paginatePoint 验证采集"] style S1 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style S12 fill:#e8f0fe,stroke:#007aff,color:#1c1c1e
① 确认 Server 已启动

启动日志应包含 opc.tcp://localhost:4840/opti-test。Windows 可用 netstat -ano | findstr :4840 检查端口监听。

只绑 127.0.0.1

测试 Server 只绑定本机回环地址,驱动和 Server 必须在同一台主机。容器或其他机器不能用 localhost。

② 查询 driverId

GET /iot/driver/getDriverWithInstanceList,在返回里找 driverName=opcua,记录它的 driverId(示例 9)。

不能把驱动名 "opcua" 当作 driverId 使用。

③ getEndpoints 发现

POST /driver/api/opcua/getEndpoints
参数:discoveryUrl=opc.tcp://localhost:4840/opti-test

返回 Endpoint 列表,记录完整的 URL + 安全策略 + 模式 + 身份类型组合。

④ testConnection 试连

POST /driver/api/opcua/testConnection
用第 ③ 步选中的完整配置,建立临时连接验证参数可用。

临时连接用完即关

成功只证明参数能建立连接,不能用于后续浏览/采集/下发。

⑤ saveInstance 保存(先关闭)

POST /iot/driver/saveInstance,instanceSwitchOn=false。
把"保存配置"和"建立连接"分开,便于定位错误。

匿名模式的小坑

不用 username/password 的匿名连接,这些字段建议显式传空字符串而非 JSON null,避免平台属性转 Map 时空指针。

⑥ 查询 instanceId

再次调 getDriverWithInstanceList,在 instanceList 里按名称找到新实例的 instanceId(如 2079755617307570178)。

⑦ openDriverInstance 启动

POST /iot/driver/openDriverInstance/{instanceId}
平台发布打开事件,驱动创建长期 Session。

success=true 不等于连上了

接口返回成功只表示流程没抛异常,底层失败不会传到 Controller。必须继续第 ⑧ 步确认状态。

⑧ 确认 instanceStatus=1

等 1 个心跳周期(约 4 秒),查 getDriverWithInstanceList,目标实例应返回 instanceStatus=1(RUNNING)。

短暂 OFFLINE 正常,持续 OFFLINE 查驱动日志。

⑨⑩ browseNodes 浏览 + 收集点位

POST /driver/api/opcua/browseNodes,从 RootFolder 逐层展开到 Scalar 目录,找到 6 个测试 Variable。

flowchart LR R["RootFolder"] --> O["Objects"] O --> SC["Scalar 目录"] SC --> T["Temperature ✓"] SC --> H["Humidity ✓"] SC --> SP["Setpoint ✓"] SC --> RN["Running ✓"] SC --> C["Counter ✓"] SC --> M["Message ✓"] style T fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style SP fill:#e8f8e8,stroke:#34c759,color:#1c1c1e

记录要导入点的 expandedNodeId,例如 nsu=urn:sctech:opti:opcua:long-running-test;s=Scalar/Temperature。

⑪ importNodes 导入

POST /driver/api/opcua/importNodes,把选中的 expandedNodeId 作为 nodes JSON 数组提交。

驱动重新读 Server 元数据校验,只导入可读/可写标量 Variable。

⑫ paginatePoint 验证采集

POST /iot/driver/paginatePoint 查询新点位。initialReadEnabled=true 时应立即看到当前值。

成功的三项铁证

① 日志"实例已连接" ② instanceStatus=1 ③ curValue 有值且 curValueTime 持续更新。三项齐全才算真正跑通。

11

5 分钟极简教程

目标:5 分钟看到温度值变化

用平台内置的 Milo 测试 Server,跳过复杂的证书配置,5 分钟内体验一次完整的"接入 → 采集 → 看到数据"。

前提条件

平台已启动(日志能看到 Milo 测试 Server 启动);你有一个启用状态的测试项目;本节跳过安全配置,用匿名连接。

填这张表就行(实例属性)

在平台创建 OPC UA 驱动实例时,只需要关心这几个关键值,其余用默认:

属性填什么为什么
endpointUrlopc.tcp://localhost:4840/opti-test测试 Server 地址
securityPolicyNONE测试用,不加密
messageSecurityModeNONE配合 NONE 策略
identityTypeANONYMOUS匿名身份最简单
username空字符串 ""匿名不需要
passwordSecretRef空字符串 ""匿名不需要
securityProfileId空字符串 ""无安全档案

✓ 其余 15 个属性全用默认值即可。

操作四步走
flowchart LR A["① 创建实例
switchOn=false"] --> B["② 启用实例
switchOn=true"] B --> C["③ 浏览到 Scalar
导入 Temperature"] C --> D["④ 查询点位
看到温度变化"] style A fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style D fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
第 1 步:创建实例
填上表属性,instanceSwitchOn=false 保存。记住实例名。
第 2 步:启用实例
把开关打开,等几秒,确认状态变 RUNNING。
第 3 步:浏览 + 导入
浏览到 Objects → Scalar,选 Temperature 点导入。
第 4 步:看数据
查询点位,curValue 应在 20.0~29.9 间变化。
恭喜!你已经完成了第一次 OPC UA 接入

看到温度值变化,说明 Discovery → Session → 订阅 → 推送整条链路都通了。接下来可以尝试导入 Setpoint 并下发一个值,体验完整的读写闭环。

卡住了?看这里
卡在哪一步检查什么
实例一直 OFFLINE看日志有无"实例已连接";端口 4840 是否监听
浏览不到 Scalar从 RootFolder → Objects → 逐层展开,方向选 FORWARD
导入返回 FAILED确认选的是 Variable(selectable=true),不是 Object 文件夹
导入成功但值不变切 collectMode=POLLING 验证(测试 Server 订阅可能有已知问题)
12

协议选型对比——什么时候用 OPC UA

三大工业协议对比

新员工常问:"同样是接设备,OPC UA、Modbus、MQTT 我该选哪个?" 看场景定:

维度OPC UAModbusMQTT
适合场景复杂信息模型、安全要求高、PLC/SCADA简单寄存器读写、仪表/变频器物联网网关消息上报
数据组织树状地址空间(灵活)线性寄存器表(简单)Topic 订阅(松耦合)
采集方式订阅为主(实时)主从轮询发布订阅
安全性内置证书 + 加密(强)弱(无内置安全)依赖 TLS(中等)
厂商支持主流 PLC 原生支持几乎所有设备支持网关/IoT 设备
配置复杂度中(需 Browse + 导入)低(填地址即可)低(配置 Topic)
本平台驱动本文档Modbus TCP/RTUMQTT 网关
决策流程图
flowchart TD Q1{"设备支持什么协议?"} -->|"OPC UA"| A["用 OPC UA 驱动"] Q1 -->|"只有 Modbus"| B["用 Modbus 驱动"] Q1 -->|"MQTT 网关"| C["用 MQTT 驱动"] Q1 -->|"都不支持"| D["加协议网关转换"] A --> A1{"数据点很多
或安全要求高?"} A1 -->|"是"| A2["✓ OPC UA 是最佳选择"] A1 -->|"简单读写"| A3["OPC UA 也能胜任
但 Modbus 更轻量"] style A2 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
一句话建议

设备原生支持 OPC UA 就优先用 OPC UA(信息模型清晰、安全强);老旧设备只有 Modbus 接口就用 Modbus;网关汇聚多设备用 MQTT。三者不是互斥的,一个项目可以混用多个驱动。

13

常见厂商适配指引

主流厂商接入要点

serverVendor 属性支持指定厂商档案,帮助驱动做兼容性处理。以下是几个常见厂商的接入经验:

西门子 Siemens
产品:S7-1200/1500 PLC、WinCC
Endpoint:默认 opc.tcp://IP:4840
注意:需在 TIA Portal 启用 OPC UA Server;证书模式建议 Basic256Sha256;证书要导入 PLC 信任列表
倍福 Beckhoff
产品:TwinCAT 3、CX 系列控制器
Endpoint:opc.tcp://IP:4840
注意:TwinCAT 自带 OPC UA Server;变量需勾选 "OPC UA" 发布;符号名即 NodeId 标识
Kepware (KEPServerEX)
定位:协议转换网关,汇聚多种下层设备
Endpoint:opc.tcp://IP:49320(非默认端口)
注意:常见端口是 49320 不是 4840;空间按"通道.设备.标签"分层
Ignition (Inductive Automation)
定位:SCADA 平台,自带 OPC UA Server
Endpoint:opc.tcp://IP:4096
注意:标签路径即 NodeId;支持匿名但生产建议开安全
CODESYS
定位:软 PLC 运行时,多品牌工控机内置
Endpoint:opc.tcp://IP:4840
注意:需在工程添加 OPC UA Server 设备;变量要勾选发布到 OPC UA
施耐德 Schneider
产品:M580 PLC、EcoStruxure
Endpoint:opc.tcp://IP:4840
注意:需在 Control Config 里启用 OPC UA;变量需显式发布
厂商接入通用清单
不管哪个厂商,接入前都要确认这 5 件事

① OPC UA Server 已启用(很多设备默认关闭)→ ② 知道 Endpoint 地址和端口(不一定是 4840)→ ③ 决定用匿名还是凭据(生产建议凭据)→ ④ 安全连接需准备双向证书 → ⑤ 确认要采的变量已发布到地址空间。

14

术语速查表

OPC UA 核心术语(随时回查)

OPC UA 术语密度高,遇到不记得的概念,回这张表查就行。

术语生活比喻说明
Discovery URL门口告示牌查询 Server 提供哪些连接入口的地址
Endpoint入口详情实际连接地址 + 安全策略 + 消息模式 + 身份方式
SecureChannel过安检对消息签名/加密/校验证书的安全通道
Session取号办事Client 登录后的逻辑会话,期间操作都在此会话上
Address Space图书馆Server 暴露的一棵节点树
Node / NodeId书 / 索书号节点 / 当前会话内定位节点的地址
NamespaceIndex阅览室编号NodeId 的数字下标,不稳定,重启可能变
NamespaceUri阅览室门牌稳定的命名空间 URI,本驱动用它替代易变的编号
ExpandedNodeId带全址的索书号携带 NamespaceUri 的稳定节点表示
Variable能读值的书有当前值可供采集/下发的节点
Subscription推送频道Server 按发布周期推送变化数据的订阅
MonitoredItem被盯的点Subscription 中具体被监控的 Variable
DataValue完整包裹值 + StatusCode + 时间戳的组合
StatusCode质量标签数据质量 Good / Uncertain / Bad
ContinuationPoint翻页游标分页 Browse 的临时游标,用完必须释放
15

已知边界

当前驱动不支持的能力
OPC UA Server
本驱动只是 Client
数组与 IndexRange
只支持标量 valueRank=-1
复杂类型解码
ByteString/Structure/ExtensionObject
Method/Event/Alarm
方法调用/事件/告警条件
Historical Access
历史读取/PubSub
递归整树导入
需手动 Browse 选择
直接写数据库
只通过 PlatformClient
trustAll
生产环境明确禁止

新增厂商兼容应实现 vendor profile 并记录厂商/产品/版本/测试日期;不能用散落 if/else、关证书验证或动态 SQL 解决现场差异。